﻿
using System;
using System.Linq;

namespace EmperialApps.WeatherSpark.Data {

    /// <summary>Manages and saves forecast history for a set of scheduled locations.</summary>
    public sealed partial class ForecastManager {

        private const double MinimumInterval = 0.25;
        private const double MaximumInterval = 8.0;

        private readonly object _schedulerLock = new object( );
        private double _interval = MinimumInterval;

        partial void InitializeInstance( ) {
            this.Timer.Tick += this.OnTimerTick;
            this.Store.DownloadCompleted += this.OnDownloadCompleted;

            // Populate scheduler with all stored locations, in random order.
            if( this.InitializeScheduler ) {
                var random = new Random( );
                var existingLocations =
                    this.Store.GetStoredLocations( )
                        .OrderBy( _ => random.Next( ) );
                foreach( Coordinate location in existingLocations )
                    this.Scheduler.AddLocation( location );
            }

            this.RescheduleTimer( );
        }


        private void RescheduleTimer( ) {
            this.Timer.Start( TimeSpan.FromSeconds( this._interval ) );
        }

        private void Unlocked_UpdateSchedule( Coordinate specifiedLocation, Coordinate? updateLocation ) {
            // If forecast download failed, remove location from schedule.
            if( !updateLocation.HasValue )
                this.Scheduler.Remove( specifiedLocation );
            // Otherwise, schedule next forecast download.
            else
                this.Scheduler.Schedule( specifiedLocation, updateLocation.Value );
        }


        private void OnTimerTick( object sender, EventArgs e ) {
#if DEBUG
            this.Timer.Stop( );
#endif
            // If any scheduled locations are ready, begin download.
            Coordinate location;
            if( this.Scheduler.TryGetNext( out location ) ) {
                this.Store.BeginDownload( location );
                this._interval = MinimumInterval;
            }
            // Otherwise, back off timer for next check.
            else if( this._interval < MaximumInterval ) {
                this._interval *= 2.0;
            }

            this.RescheduleTimer( );
        }

        private void OnDownloadCompleted( object sender, ForecastEventArgs e ) {
            if( e.Error == null ) {
                // Reschedule next forecast download.
                Forecast forecast = e.Forecast;
                Coordinate location = forecast.Location;
                lock( this._schedulerLock )
                    this.Unlocked_UpdateSchedule( e.Location, location );

                // Combine data with any existing forecast and save.
                Forecast existing = this.Store.Load( location, e );
                if( existing != null && existing.Start != forecast.Start ) {
                    // If new forecast has less precision, keep existing if it still covers relevant range.
                    if( forecast.Interval > existing.Interval && forecast.Start < existing.End( ) ) {
                        forecast = existing;
                    }
                    else {
                        DateTimeOffset yesterday = forecast.Start.GetDateOffset( ).AddDays( -1 );
                        int maximumHours = (int)forecast.End( ).HoursFrom( yesterday );
                        Forecast combined = Forecast.Combine( forecast, existing, maximumHours );
                        forecast = combined;
                    }
                }

                e = e.UpdateForecast( forecast );
            }
            else {
                // Reschedule failed download, if forecast has succeeded in the past.
                Coordinate location = e.Location;
                Forecast existing = this.Store.Load( location, e );
                Coordinate? updateLocation = existing == null ? default( Coordinate? ) : location;
                lock( this._schedulerLock )
                    this.Unlocked_UpdateSchedule( location, updateLocation );
            }

            this.Store.Save( e );
        }

    }

}
